Skip to main content

CRUD REST API

  • We want the frontend to be able to take some photos and upload them to the server. This means we are going to end up with something that a lot of java business applications end up with, ie. we want to be able to
    • create photos,
    • read photos (like getting them and seeing specific photos)
    • update photos
    • delete photos
  • This means we are building a CRUD REST API (for "create, read, update, delete").

Step 1: the Photo model class​

  • Let's create a so-called Photo model class in your package com.companyname.phth.photoz.clone/
    • For now, let's just think of a photo as a combination of an id and a filename. The image data will be added later (we just add a comment // raw data so we do not forget this).
    • For now, we will put everything into one java package and in the end we will refactor it to make it look better
    • add the following class Photo.java into com.companyname.phth.photoz.clone/
tip

Press alt insert on or in the com.companyname.phth.photoz.clone/ package to add a new class to the package.

warning

Also add getters and setters to the Photo class so that every third party dependency in Spring Boot that takes your Photos and converts them eg. to JSON (this is exactly what the get() method that we add next needs) will work without any workarounds and without any problems.

tip

You can press alt insert in the editor inside the Photo class to quickly add getters and setters for each data member.

Photo.java
package com.companyname.phth.photoz.clone;

public class Photo {

private String id;

private String filename;

// raw data


public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getFilename() {
return filename;
}

public void setFilename(String filename) {
this.filename = filename;
}
}

Step 2: Read a Photo​

  • when writing a REST service it helps to "start from the top", which means that
    • at some point what we want to have is a @GetMapping("/photoz"), ie. we want our PhotozController to react to the URL "/photoz"
      • @GetMapping("/photoz") means that we want to return any photo(s) that our database has under the URL "/photoz" (at the moment we do not have a database yet - but this does not really matter, we just write out what we want our app to do)
      • we want to return the photos as a List<Photo>, so the get() method that is mapped to "/photoz" should have the return type List<Photo>
      • we do not have any photos yet, so we add a list of photos in the form of a List<Photo> database db
      • to this database we add a new photo via new Photo() (but for this we need a new Photo() constructor)
PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class PhotozController {

private List<Photo> db = List.of(new Photo(/* TODO */));

@GetMapping("/")
public String hello() {
return "Hello World!";
}

@GetMapping("/photoz")
public List<Photo> get() {
/* TODO */
}
}
  • the new Photo() needs an id and a filename, so we have to add a new constructor to our Photo class
tip

Again, press alt insert in the editor to add new constructors to the Photo class. You can also choose which data members the constructor should initialize or to just add an empty constructor.

warning

Always make sure to also have an empty constructor because, otherwise, you will have some issues with converting your java class to JSON and vice versa (see later).

Photo.java
package com.companyname.phth.photoz.clone;

public class Photo {

private String id;

private String filename;

public Photo() {
}

public Photo(String id, String filename) {
this.id = id;
this.filename = filename;
}

// raw data


public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getFilename() {
return filename;
}

public void setFilename(String filename) {
this.filename = filename;
}
}
  • let's give the new photo an id and a filename and let the get() method return the list of (mocked) photos db
PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class PhotozController {

private List<Photo> db = List.of(new Photo("1", "hello.jpg"));

@GetMapping("/")
public String hello() {
return "Hello World!";
}

@GetMapping("/photoz")
public List<Photo> get() {
return db;
}
}
  • then, when we rerun (ctrl shift F5) the app we will get a success message because Spring is smart enough to understand that get() should return our list of photos and then, internally, Spring Boot will take the list and convert it to JSON, so that we can see the list in our browser (in JSON format as an array)
  • let's continue with GET methods
  • next, we also want to be able to get a specific photo
  • for convenience, let's duplicate (copy and paste) the get() method because we want to change the GetMapping because we know that we want to have some sort of {id} parameter behind /photoz to access a specific photo
  • at the moment we only have the id="1", so everything else should give us a 404 which means that we have to parse this {id} somehow into the method, in other words the method should look like this: get(String id).
    • We do this by using the marker annotation @PathVariable which tells Spring Boot that if there is some parameter with curly braces in the @GetMapping and if there is also a method parameter with the same name then Spring Boot should just substitute the parameter value into the method's String parameter
PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class PhotozController {

private List<Photo> db = List.of(new Photo("1", "hello.jpg"));

@GetMapping("/")
public String hello() {
return "Hello World!";
}

@GetMapping("/photoz/{id}")
public List<Photo> get(@PathVariable String id) {
return db;
}
}
  • our method should not return a List<Photo>, but only one specific Photo or a 404 error message
  • problem: we cannot return our list of photos db any more
    • so, we will rewrite things a little bit because this will make it simpler in the long run
    • instead of having a list of photos List<Photo> we will have a map of photos Map<String, Photo>, so our db is now a new Hashmap<> (as always, make sure the import is right) and we quickly (and dirtily) initialize the Hashmap by new HashMap<>() {{ put("1", new Photo("1", "hello.jpg")); }}; and this will be our database
    • in the Photo get(String id) method we just call a .get(id) on the Hashmap, ie. we just return a specific photo
    • in the List<Photo> get() method we call .values() on the Hashmap which will give us all the different photos from the Hashmap
      • problem: .values() returns the photos as a java.util.Collection
        • so, instead of List<Photo> get() we will use Collection<Photo> get()
tip

You can simply press altenter (VSCode: ctrl.) on .values() or click on IntelliJ's red light bulb and select "Make get() return java.util.Collection" to change the return type of get().

PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class PhotozController {

private Map<String, Photo> db = new HashMap<>() {{
put("1", new Photo("1", "hello.jpg"));
}};

@GetMapping("/")
public String hello() {
return "Hello World!";
}

@GetMapping("/photoz")
public Collection<Photo> get() {
return db.values();
}

@GetMapping("/photoz/{id}")
public Photo get(@PathVariable String id) {
/* TODO */
return db.get(id);
}
}
  • db.get(id) only works when we have a photo available, but when we do not have one available we do not want to return a NULL value, but we do want to return a 404
    • in Spring we do that by throwing specific exceptions, eg., here, we can throw new ResponseStatusException(HttpStatus.NOT_FOUND);
PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class PhotozController {

// ...

@GetMapping("/photoz/{id}")
public Photo get(@PathVariable String id) {
Photo photo = db.get(id);
if (photo == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
return photo;
}
}

Step 3: Delete a Photo​

  • let's add a method to delete a Photo when we receive a DELETE request:
    • for convenience, we just copy and paste the get(@PathVariable String id) method and modify it
PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class PhotozController {

// ...

@DeleteMapping("/photoz/{id}")
public void delete(@PathVariable String id) {
Photo photo = db.remove(id);
if (photo == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
}
  • problem: so far we have executed GET calls by refreshing the page, but how do we execute a DELETE call? How do we delete a Photo using the browser?
    • IntelliJ has a REST Client which we could use in the IntelliJ Ultimate Edition
    • instead, we will use the bare bones version of a DELETE request by using the JavaScript Fetch API
  • run the following JavaScript statement using the DevTools/JavaScript Console of your browser
warning

In Firefox before running the following JavaScript code you have to (temporarily) disable devtools.jsonview.enabled in about:config, otherwise Content Security Policy (CSP) will block this request, stackoverflow

JavaScript Fetch API DELETE Request
(async function deletePhoto(id) {
await fetch("http://localhost:8080/photoz/" + id, {
method: "DELETE"
})
})("5")
info

Here, "5" is passed as the id to the method deletePhoto(id).

  • the console will output a 404 because the Photo with id="5" does not exist
  • what happens if we send a DELETE request for the id="1" instead of id="5"?
    • then, when we refresh the http://localhost:8080/photoz/1 page the page will show a 404 error message because the photo with id="1" is gone and the app just returned an empty list of Photos
    • when we load the http://localhost:8080/photoz page (without the 1 at the end) we will see the empty list of Photos
    • this is exactly what we wanted

Step 4: Create a Photo​

  • again, for convenience, we just copy and paste the delete() method
  • the frontend should not generate the id, but we should generate the id
    • for now, we will set the id to some UUID.randomUUID() (a random long string)
  • we also return the Photo (effectively, the JSON object) with the newly set id
  • we will not do any validation for now (→ Step 5)
quick fix

After writing return photo; you can just hit altenter (VSCode: ctrl.) to change the return type of the create() method from void to Photo more quickly.

reformat frequently

Don't worry about formatting. After writing the code you can always press ctrlaltl (VSCode: altshiftf) to reformat the code. Press this shortcut frequently to keep your code well-formatted.

PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class PhotozController {

private Map<String, Photo> db = new HashMap<>() {{
put("1", new Photo("1", "hello.jpg"));
}};

@GetMapping("/")
public String hello() {
return "Hello World!";
}

@GetMapping("/photoz")
public Collection<Photo> get() {
return db.values();
}

@GetMapping("/photoz/{id}")
public Photo get(@PathVariable String id) {
Photo photo = db.get(id);
if (photo == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
return photo;
}

@DeleteMapping("/photoz/{id}")
public void delete(@PathVariable String id) {
Photo photo = db.remove(id);
if (photo == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}

@PostMapping("/photoz")
public Photo create(Photo photo) {
photo.setId(UUID.randomUUID().toString());
db.put(photo.getId(), photo);
return photo;
}
}
  • restart the webserver
  • let's send a POST request, again, via the JavaScript Fetch API
    • let photo = {"fileName": "hello.jpg"}; means we have one photo which should be created
      • here, we are not sending in an "id", but only a "filename"
    • we are executing a fetch() call to localhost:8080/photoz
    • we are going to send in a "Content-Type": "application/json" because we need to tell Spring Boot what we are sending to it
      • Spring Boot will then understand that "application/json" means that it is getting JSON and that it needs to convert the JSON to a java Photo object
    • body: JSON.stringify(photo) means we will send in the photo as a String in the body
    • finally, we print out whatever we get back from the server with .then(result => result.text()).then(text => alert(text));
typo

There was a typo in the Fetch API POST Request: instead of "fileName" you have to use "filename".

JavaScript Fetch API POST Request
(async function createPhoto() {
// wrong: let photo = {"fileName": "hello.jpg"};
let photo = {"filename": "hello.jpg"};

await fetch("http://localhost:8080/photoz", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(photo)
})
.then(result => result.text())
.then(text => alert(text));
})();

Note: You can copy and paste this including all comments into the console. Below is the same code without comments.

JavaScript Fetch API POST Request (typo corrected)
(async function createPhoto() {
let photo = {"filename": "hello.jpg"};

await fetch("http://localhost:8080/photoz", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(photo)
})
.then(result => result.text())
.then(text => alert(text));
})();
  • this will output an alert popup {"id":"caf92f05-abeb-4c1f-8acf-76dc83eaadab","filename":null} in the browser
  • when we refresh http://localhost:8080/photoz the page will show [{"id":"1","filename":"hello.jpg"},{"id":"caf92f05-abeb-4c1f-8acf-76dc83eaadab","filename":null}]
  • this looks almost perfect, the new photo was created, but the "filename" is null, why?
    • this is a common beginner's mistake: you should not pass the Photo object as argument to the create() method, but instead, since we are sending in JSON data, we need to use, again, a marker annotation, namely @RequestBody to tell Spring Boot that it should take the JSON data and convert it to the Photo class
warning
  • Always make sure that you have the right marker annotations in these methods and RestControllers.
  • important annotations you should remember:
    • @RequestBody
    • @PathVariable
    • @GetMapping
    • @PostMapping
PhotozController.java
package com.companyname.phth.photoz.clone;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class PhotozController {

// ...

@PostMapping("/photoz")
public Photo create(@RequestBody Photo photo) {
photo.setId(UUID.randomUUID().toString());
db.put(photo.getId(), photo);
return photo;
}
}
  • let's check if this fixes the previous "filename":null problem
  • restart the webserver
    • note: we are resetting our database with every restart
  • execute the JavaScript createPhoto() function again
  • this time, the page at http://localhost:8080/photoz will show [{"id":"1","filename":"hello.jpg"},{"id":"c34c3c26-bec1-4c31-b941-877fe68ef32b","filename":"hello.jpg"}
    • this means we have two photos in our database
  • if you execute the POST request again, it will show [{"id":"8a022ae2-9eaf-48db-87c2-8ec1fa4787f8","filename":"hello.jpg"},{"id":"1","filename":"hello.jpg"},{"id":"c34c3c26-bec1-4c31-b941-877fe68ef32b","filename":"hello.jpg"}]
    • this means we have three photos in our database and we already get some JSON messages back

Step 5: Validation​

  • right now we are only uploading metadata and not real image data and we will change that below, but before that let's do one other important thing
  • we have no validation yet
  • at the moment someone can send an "id" and it gets overwritten anyway with a random "id" (thus, the "id" field is safe), but, the issue is, you can send in a photo without a "filename"
    • try this with the POST request we used previously by writing let photo = { }; instead of let photo = {"filename": "hello.jpg"};
      • this will add a photo to the database that has the filename null
      • how do we prevent this from happening?
  • we need an additional dependency:
    • open the pom.xml file
    • as you can see, all Spring Boot dependencies (in our project, spring-boot-starter-data-jdbc and spring-boot-starter-web) have the same groupId, <groupId>org.springframework.boot</groupId>
    • what we need is spring-boot-starter-validation, so we add the following lines to our pom.xml
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- ... -->

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<!-- ... -->

</project>
  • then click the "Load Maven Changes" icon in the upper right corner so that IntelliJ reloads the pom.xml file automatically (IntelliJ will download all dependencies and will index them)
  • now your app has validation capabilities
  • now you can add the @Valid annotation to the Photo photo argument of the Photo create() method
    • what does @Valid mean?
      • Eg. you can specify @NotEmpty on the filename in the Photo class which means that the filename should not be empty
      • there are many more annotations like @NotEmpty
      • when you look at the jakarta.validation.constraints package (click on constraints in import jakarta.validation.constraints.NotEmpty; while pressing and holding ctrl, this will open the jakarta.validation.constraints folder in the file explorer where you can find NotEmpty.java and other validation constraints) you can see that there are other marker annotations such as @DecimalMax, @Email (to make sure something is a valid email), @Min, @Max, @Negative, @NotEmpty, @Null, @Past (if you have dates), @Positive, @PositiveOrZero, etc
      • so, you can annotate you fields with different annotations and then Spring Boot will be able to automatically validate whatever the user puts into your object/into the JSON
Photo.java
package com.companyname.phth.photoz.clone;

import jakarta.validation.constraints.NotEmpty;

public class Photo {

private String id;

@NotEmpty
private String filename;

public Photo() {
}

// ...
}
PhotozController.java
package com.companyname.phth.photoz.clone;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class PhotozController {

// ...

@PostMapping("/photoz")
public Photo create(@RequestBody @Valid Photo photo) {
photo.setId(UUID.randomUUID().toString());
db.put(photo.getId(), photo);
return photo;
}
}
  • restart the webserver and execute the POST request we used previously with the empty filename let photo = { }; again
  • this will show an alert popup {"timestamp":"2024-07-21T05:13:34.148+00:00","status":400,"error":"Bad Request","path":"/photoz"}
    • this is a 404 which says "Bad Request" which is excactly what we wanted because it says something was wrong with our picture (aka the picture was not valid)
  • next, to check if a valid POST request still works, you can try to POST a photo with some new filename, eg. by using let photo = {"filename": "helloWorld.jpg"}; instead of let photo = { };
    • this works and will add a new photo to the database

Step 6: Uploading a real photo as a base64 string (instead of just metadata)​

  • for this we will use the browser's file chooser capabilities
  • first, create a new static html page called upload.html in src/main/resources/static/ (press altinsert on the static/ folder to create the html file)
    • the html file has
      • one <input /> field: a file input field so that we can choose your files
      • an upload button which executes an uploadFile() method which is defined in the html file, too
        • uploadFile() is an async function which executes a so-called HTTP multipart request with some FormData() (here: your file) to http://localhost:8080/photoz
        • uploadFile() is just a JavaScript Fetch API POST request like the one we did above, but unlike above we are not sending JSON any more, but instead we are just letting the browser send a file
          • the browser will send the raw data and the filename with the whole request
upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>


<h2>Photo Upload</h2>

<input id="fileupload" type="file" name="fileupload" />
<button id="upload-button" onclick="uploadFile()"> Upload </button>

<script>
async function uploadFile() {
let formData = new FormData();
formData.append("data", fileupload.files[0]);
await fetch('http://localhost:8080/photoz', {
method: "POST",
body: formData
}).then(result => result.text())
.then(text => alert(text));
}
</script>
</body>
</html>
  • this means we need to change our PhotozController because at the moment the create() method takes @RequestBody @Valid Photo photo as argument, but we want MultipartFile as argument which is a Spring Boot specific class
    • when you upload a file Spring will convert the uploaded file to a MultipartFile
    • again, you need a marker annotation, @RequestPart("somePart"), which requests a part from a multipart file upload
      • you need to tell Spring Boot which part contains your file
        • formData.append("data", fileupload.files[0]) means that we are sending some formData, ie. our file fileupload.files[0], under the key "data"
        • therefore, we have to refer to the key "data" to get our file
        • we could have called the key differently, but here we just chose to call it "data"
selecting inside parentheses, brackets, braces, etc

When the cursor is placed somewhere inside parentheses press altshiftright (multiple times) to quickly select everything inside the parentheses. Eg. you can use this to replace @RequestBody @Valid Photo photo with @RequestPart("data") MultipartFile file.

  • so, we are getting a MultipartFile file in and at some point we have to create a photo, hence we write Photo photo = new Photo();
  • next we have to set the id and filename of photo:
    • the randomUUID is still fine
    • the filename has to be the original filename that the browser sends in, so we use file.getOriginalFilename() which is a method that is a part of the MultipartFile class
  • we also want to store the raw data of our photo:
    • we will store the raw data in a database later
    • we could also store the raw data in a file or whatever
    • for now, we will store the raw data in memory because this is the simplest to demonstrate
      • for this, we have to add the raw data to the Photo class:
        • for simplicity's sake, we just add a byte array which contains the photo's data private byte[] data;
        • as always do not forget to also add getters and setters for data (by pressing altinsert)
  • finally, we can just use the method file.getBytes() of the MultipartFile class to set the data of the newly created Photo object photo, ie. we write photo.setData(file.getBytes());
    • this will throw an exception (red light bulb), but we can simply add a throws IOException to the method signature to propagate the exception out to the caller of the create() method (see java exceptions) because for now, we do not want to worry about the file.getBytes() function
warning

In-memory database: When we upload a 100MB file to our app then the memory usage will increase by 100MB!

PhotozController.java
package com.companyname.phth.photoz.clone;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class PhotozController {

// ...

@PostMapping("/photoz")
public Photo create(@RequestPart("data") MultipartFile file) throws IOException {
Photo photo = new Photo();
photo.setId(UUID.randomUUID().toString());
photo.setFilename(file.getOriginalFilename());
photo.setData(file.getBytes());
db.put(photo.getId(), photo);
return photo;
}
}
  • restart the webserver
  • at http://localhost:8080/photoz: the photo we have stored in our database has no data, but that is fine because we did not set any data for that one
  • go to http://localhost:8080/upload.html
    • Spring Boot is smart enough to put upload.html at this URL because we put upload.html in src/main/resources/static
    • click "Browse..." and choose and upload some file
      • if the chosen file is large enough then the "Run" window in IntelliJ will show a .MaxUploadSizeExceededException:
IntelliJ Run window
2024-07-22T01:17:15.504+02:00  WARN 367078 --- [photoz-clone] [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded]
2024-07-22T01:17:15.504+02:00 WARN 367078 --- [photoz-clone] [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded]
  • to fix this we will add .max-file-size=100MB and .max-request-size=100MB to the src/main/resources/application.properties file:
application.properties
spring.application.name=photoz-clone
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
  • now try to upload the large image again
  • you will see that the image is uploaded successfully
  • the data field contains the image bytes converted to base64 string encoding
  • at http://localhost:8080/photoz you can see the whole image as a string
  • but we do not want to get this string data and this is what we are going to fix next

Step 7: Uploading a real photo and downloading it (or displaying it)​

  • there is two things to it
  • first, when we get the metadata we want that Spring does not include the "data" field and sends it back to the frontend
  • Spring uses a library called jackson for the JSON conversion
    • In the Photo class use a jackson specific marker annotation @JsonIgnore on the data field of the Photo class. This will prevent the conversion of data to JSON whenever we send a photo back to the frontend. So, we will not see the photo.
package com.companyname.phth.photoz.clone;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.NotEmpty;

public class Photo {

private String id;

@NotEmpty
private String filename;

@JsonIgnore
private byte[] data;

public Photo() {
}

public Photo(String id, String filename) {
this.id = id;
this.filename = filename;
}

// raw data

// ...
}
  • At the moment we can upload photos, but we do not get the data back (ie. we cannot download the data). But we want to see a real photo in our browser. How do we do that?
  • For this, we need to refactor the app quite a bit.
  • There are several ways to approach this.

DEPENDENCY INJECTION: Inject PhotozService into PhotozController and DownloadController​

  • let's say we want to have another additional controller called DownloadController
    • it is, again, a @RestController
    • the only thing this controller does is the following
    • it has a @GetMapping("/download/{id}") where id is supposed to be the id of the photo we want to download and see in our browser
    • this @GetMapping will send raw bytes back to the browser
      • we could return byte[], but instead, here we will return ResponseEntity<byte[]> because we want to send back not only the bytes, but also some HTTP headers including the filename of the file that we are requesting
      • so we want to request a specific photo and send back a ResponseEntity<byte[]> in turn, so we write public ResponseEntity<byte[]> download(@PathVariable String id)
      • we want it to return ResponseEntity<>(data, headers, HttpStatus.OK); where data will be our image bytes and HttpStatus.OK status will be send back when everything worked correctly
      • we create the data that comes from our photo in our database with byte[] data;
      • we also want some headers so we could create MultiValueMap<String, String> headers;, but there is also the specific class HttpHeaders that the Spring Framework offers, so we better write HttpHeaders headers = new HttpHeaders();
      • but how do we get the photo data now into byte[] data;?
        • this is the first time we will learn something about dependency injection
DownloadController.java
package com.companyname.phth.photoz.clone;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DownloadController {

@GetMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable String id) {

byte[] data;
HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<>(data, headers, HttpStatus.OK);
}
}
  • problem: since the database variable db is private to the PhotozController class we cannot access it in the DownloadController.
    • we could make it public or instead, we could observe the following:
      • when we layer our apps then we have our controllers and what they do is they convert JSON back to some object or they extract the PathVariables and they send back some JSON
      • but what they do not do is they do not talk to the database
      • for the latter task, we could create a so-called service class PhotozService
        • again, we need to tell Spring that this is a special class and that Spring should do something with it: we want Spring to instantiate this class for us whenever Spring boot up the app
        • we do this with the marker annotations @Component and @Service (they both do the vey same thing, it is just that @Service is more human readable, but you can choose either one of those two annotations)
        • here, we just add @Service to the PhotozService class
        • now, at startup Spring knows that it not only has to look at the REST controllers, but also at the PhotozService because it needs to be instantiated as a service
        • next, we can move the db definition from the PhotozController class to the PhotozService class
      • inside the PhotozController class:
        • now, instead of asking the database Map object inside the PhotozController class to give some db values or to give the photo for a specific id or to remove something, we let the PhotozController ask the PhotozService by adding private final PhotozService photozService; to the PhotozController class
          • we also add a constructor (by using IntelliJ context actions ctrl.) which receives a PhotozService and sets that photozService inside the PhotozController
        • then, in get() instead of asking the Hashmap db in return db.values(); we will ask the photozService by writing return photozService.get(); which is a method we still have to create (by using IntelliJ context actions ctrl.)
PhotozService.java
package com.companyname.phth.photoz.clone;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

//@Component
@Service
public class PhotozService {
private Map<String, Photo> db = new HashMap<>() {{
put("1", new Photo("1", "hello.jpg"));
}};
}
PhotozController.java
package com.companyname.phth.photoz.clone;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class PhotozController {

private final PhotozService photozService;

public PhotozController(PhotozService photozService) {
this.photozService = photozService;
}

@GetMapping("/")
public String hello() {
return "Hello World!";
}

@GetMapping("/photoz")
public Collection<Photo> get() {
return photozService.get();
}

// ...
}
constructor injection

Now, in PhotozController.java you can see that you have some delegation inside the PhotozController class: The PhotozController does not know about the database any more, but the PhotozController only knows about the PhotozService. The PhotozController asks the PhotozService to return all the photos the PhotozService knows. It does not matter if the photos come from a database or from a filesystem (eg. S3) or whatever, the PhotozController does not change.

How does the PhotozController get a PhotozService instance? You already know that. On startup Spring scans your entire package and it has a look at specific classes like @RestController. If Spring finds a @RestController it creates a new @RestController in which the methods respond to HTTP calls. The same thing happens when Spring finds a constructor with some requirement, eg. in the PhotozController class Spring will find the constructor public PhotozController(PhotozService photozService) which has the requirement PhotozService photozService, ie. the constructor needs a PhotozService. This is so-called constructor injection. Then, Spring will scan your classes and look for a class named PhotozService (and it is even annotated with the @Service annotation). So, Spring will find PhotozService and create a new PhotozService, just a single instance of it (singleton). Finally, Spring will inject it into the PhotozController so that the PhotozController can call any method on that service as it wants.

injection styles

There are different injection styles in Spring, but they all do the same thing, namely inject a PhotozService into the PhotozController:

constructor injection:

private final PhotozService photozService;

public PhotozController(PhotozService photozService) {
this.photozService = photozService;
}

field injection (style 1):

@Autowired
private final PhotozService photozService;

field injection (style 2):

private final PhotozService photozService;

public PhotozController(@Autowired PhotozService photozService) {
this.photozService = photozService;
}
  • next, the DownloadController needs the same thing, so we just copy and paste the lines we added to the PhotozController class:
    • after copying and pasting you have to fix a small error, the PhotozController constructor needs to be renamed to DownloadController (by using IntelliJ context actions ctrl.)
  • now, the DownloadController also gets a PhotozService injected and can use the PhotozService to retrieve a specific photo
DownloadController.java
package com.companyname.phth.photoz.clone;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DownloadController {
private final PhotozService photozService;

public DownloadController(PhotozService photozService) {
this.photozService = photozService;
}

@GetMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable String id) {

byte[] data;
HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<>(data, headers, HttpStatus.OK);
}
}
  • in the PhotozController class we need to replace all calls of db with photozService
  • but photozService does not have the methods that the Hashmap db had like eg. the method db.remove(), db.put(), etc.
  • so we have to create those methods
tip

Let IntelliJ create the missing methods for you (by using IntelliJ context actions ctrl.).

PhotozService.java
package com.companyname.phth.photoz.clone;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

//@Component
@Service
public class PhotozService {
private Map<String, Photo> db = new HashMap<>() {{
put("1", new Photo("1", "hello.jpg"));
}};

public Collection<Photo> get() {
return null;
}

public Photo get(String id) {
}

public Photo remove(String id) {
}

public void save(String id, Photo photo) {
}

}
  • so, what should these methods do?
  • mostly, we can just copy and paste the code that was in the PhotozController class before
  • in save() we have to rearrange a little bit
PhotozService.java
package com.companyname.phth.photoz.clone;

import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

//@Component
@Service
public class PhotozService {
private Map<String, Photo> db = new HashMap<>() {{
put("1", new Photo("1", "hello.jpg"));
}};

public Collection<Photo> get() {
return db.values();
}

public Photo get(String id) {
return db.get(id);
}

public Photo remove(String id) {
return db.remove();
}

public Photo save(String fileName, byte[] data) {
Photo photo = new Photo();
photo.setId(UUID.randomUUID().toString());
photo.setFilename(fileName);
photo.setData(data);
db.put(photo.getId(), photo);
return photo;
}

}
PhotozController.java
package com.companyname.phth.photoz.clone;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class PhotozController {

private final PhotozService photozService;

public PhotozController(PhotozService photozService) {
this.photozService = photozService;
}

@GetMapping("/")
public String hello() {
return "Hello World!";
}

@GetMapping("/photoz")
public Collection<Photo> get() {
return photozService.get();
}

@GetMapping("/photoz/{id}")
public Photo get(@PathVariable String id) {
Photo photo = photozService.get(id);
if (photo == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
return photo;
}

@DeleteMapping("/photoz/{id}")
public void delete(@PathVariable String id) {
Photo photo = photozService.remove(id);
if (photo == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}

@PostMapping("/photoz")
public Photo create(@RequestPart("data") MultipartFile file) throws IOException {
return photozService.save(file.getOriginalFilename(), file.getBytes());
}
}
  • now the PhotozController and the DownloadController are using the very same instance of the PhotozService and that is what the whole constructor injection and dependency injection topic is about

Test the DEPENDENCY INJECTION​

  • let's also fix our DownloadController now, so we can quickly test our changes
  • as a "quick and dirty" fix, we will temporarily change byte[] data; to byte[] data = new byte[0]; (by using IntelliJ context actions ctrl.), but we will change this later
DownloadController.java
// imports

@RestController
public class DownloadController {
private final PhotozService photozService;

public DownloadController(PhotozService photozService) {
this.photozService = photozService;
}

@GetMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable String id) {

byte[] data = new byte[0];
HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<>(data, headers, HttpStatus.OK);
}
}
  • now let's quickly check if our dependency injection is working
  • restart the webserver
  • http://localhost:8080/photoz will show [{"id":"1","filename":"hello.jpg"}], so we got rid of the "data" field that we had before
  • http://localhost:8080/photoz/1 will show {"id":"1","filename":"hello.jpg"}, so we can still search for a specific photo
  • we can also still get all photos or remove the photos
  • that means our dependency injection is working perfectly

Fix the DownloadController​

  • now, let's continue fixing our DownloadController
  • at the moment our download() method gets in a String id, so we will try to get a photo for that specific id by writing Photo photo = photozService.get(id);
  • again, we have to check if the photo exists and if it does not exist we have to throw a HttpStatus.NOT_FOUND ResponseStatusException
  • now, we can set the data simply to photo.getData()
  • the line return new ResponseEntity<>(data, headers, HttpStatus.OK); means that we can almost download the photo now, but the only thing that is still missing in there are the headers
  • there are two things three things we need to do in order to set the headers (set the ContentType, filename and ContentDisposition):
    • we need to tell that our photo is of a specific ContentType, ie. we need to say whether we have .jpg, .png or whatever have you
    • we can set this ContentType by calling headers.setContentType();
    • problem: we do not store the ContentType in our Photo class yet, so we have to add it (with getters and setters)
      Photo.java
      package com.companyname.phth.photoz.clone;

      import com.fasterxml.jackson.annotation.JsonIgnore;
      import jakarta.validation.constraints.NotEmpty;

      public class Photo {

      private String id;

      @NotEmpty
      private String filename;

      private String contentType;

      // ...

      public String getContentType() {
      return contentType;
      }

      public void setContentType(String contentType) {
      this.contentType = contentType;
      }
      }
    • then, in the DownloadController we can just get the ContentType by calling the Spring specific method MediaType.valueOf() (you kind of have to know all these methods by heart, don't worry if you think "how did he come up with that?") and we want to send photo.getContentType() back to the browser, so summarizing we write headers.setContentType(MediaType.valueOf(photo.getContentType()));
      • so, now the browser knows if it is a .gif or a .png or whatever
    • there is another thing the browser needs: the filename
      • at the moment, the browser does not have a filename, so we need to send a header with a filename
    • we also need to tell the browser if the browser should display the photo in the browser or automatically download it
      • you do this with headers.setContentDisposition();
      • in order to set a ContentDisposition you need to build a new one by calling ContentDisposition.builder().build()(again, just a Spring specific wrapper around HTTP specific header methods)
      • then, you can just write ContentDisposition.builder("attachment").build() to tell the browser that it should download the photo instead of displaying it in the browser (otherwise, if you want to display the photo you could just write ContentDisposition.builder("inline").build())
      • we specify the filename by adding ContentDisposition.builder("attachment").filename(photo.getFilename()).build();
introduce variable

Press ctrlaltv to introduce a new variable for a selected expression. Eg., here, you can select ContentDisposition.builder().build() and then press ctrlaltv to introduce a new variable for the newly built ContentDisposition.

DownloadController.java
package com.companyname.phth.photoz.clone;

import org.springframework.http.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
public class DownloadController {
private final PhotozService photozService;

public DownloadController(PhotozService photozService) {
this.photozService = photozService;
}

@GetMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable String id) {

Photo photo = photozService.get(id);
if (photo == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
byte[] data = photo.getData();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf(photo.getContentType()));
ContentDisposition build = ContentDisposition
.builder("attachment")
.filename(photo.getFilename())
.build();
headers.setContentDisposition(build);
return new ResponseEntity<>(data, headers, HttpStatus.OK);
}
}
  • the last thing we need to do is storing the ContentType using the PhotozService
    • the browser uploads the file with the filename and the ContentType, we just need to save the ContentType once the file is created
  • for this, we add a third argument to the photozService.save(); call in the create() method of the PhotozController class by writing photozService.save(file.getOriginalFilename(), file.getContentType(), file.getBytes());
    • and then we let IntelliJ add a third parameter String ContentType to the old photozService.save() method (by using IntelliJ context actions ctrl. on photozService.save();) so that photozService.save(); can actually be called with three arguments
    • then, we can set the ContentType in the save() method of the PhotozService class by adding photo.setContentType(contentType); to the save() method
PhotozController.java
// imports

@RestController
public class PhotozController {

// ...

@PostMapping("/photoz")
public Photo create(@RequestPart("data") MultipartFile file) throws IOException {
return photozService.save(file.getOriginalFilename(), file.getContentType(), file.getBytes());
}
}
PhotozService.java
// imports

//@Component
@Service
public class PhotozService {
// ...

public Photo save(String fileName, String contentType, byte[] data) {
Photo photo = new Photo();
photo.setId(UUID.randomUUID().toString());
photo.setFilename(fileName);
photo.setContentType(contentType);
photo.setData(data);
db.put(photo.getId(), photo);
return photo;
}

}

Test the App​

  • restart the webserver
  • try the following tests:
    • http://localhost:8080/photoz/1: the photo is here, but it has no contentType. That is absolutely fine because this is our mock photo.
    • http://localhost:8080/upload.html:
      • upload a file
      • the alert box will show eg. {"id":"20d04d80-a85a-4066-875c-45d8c6c5dbb6","filename":"Angela_Merkel_freigestellt_ohneKopf.png","contentType":"image/png"}
        • so, the filename and contentType work flawlessly
      • to check if the id works
        • copy the id shown in the alert box and go to the download URL, eg. http://localhost:8080/download/20d04d80-a85a-4066-875c-45d8c6c5dbb6
        • a photo will be downloaded by the browser
        • check if the downloaded photo is the one you uploaded before,
          • result: this works

Recap​

  • we have built
    • a couple of controllers, namely DownloadController and PhotozController
    • an application class, PhotozCloneApplication
    • a service, PhotozService
    • a so-called model class, Photo

Step 8: Refactor the project​

  • at the moment, this all lives in one folder, or rather in one package
  • we want to refactor the project a bit so that it is a bit more idiomatic
  • one way to do that is to introduce new subpackages, eg. web (for the controllers), service (for the PhotozService.java), model (for Photo.java), whereas the application PhotozCloneApplication.java stays at the top
  • there are more ways to structure this, but for our tiny package we can take this as a good starting structure